package au.edu.swin.MelbourneMap;

import java.io.*;
import java.util.*;
import java.awt.geom.*;

/**
 * Implementation of Model class using Model View Controller architecture.
 *
 * @author  Alexis Wellavize (s/n: 6033482)<br/>
 *           Dotan Tamir (s/n: 6532969)
 * @since   v1, created:  17/09/2012
 * @version v2, modified:  22/09/2012
 *
 */
 
public class Model
{
    // Hashmap containing regions read from file
    private HashMap<String, Region> regions;
    // Hashmap containing roads read from file
    private HashMap<String, Road> roads;
    // Easting and northing values
    private Double easting, northing;
    // Maximum and minimum easting and northing values
    private Double maxE, maxN, minE, minN;
    // Scale factors for X and Y axis
    private Double xScale, yScale;
    // Translation for X and Y axis
    private Double xTranslate, yTranslate;
    // Width and height of map
    private Integer height, width;
    // Scenario filename loaded
    private String scenarioFileName;
    // Label visibility flag
    private boolean labelVisibility;
    //set to true when senario file is read
    private boolean fileIsLoaded;
    private int transparencyFactor;
    // Flags to determine whether data has changed
    private boolean roadDataChanged;
    private boolean regionDataChanged;
    private boolean railDataChanged;
	// List of filenames of files opened from scenario file
    private ArrayList<String> filenames;
	// List of connected nodes
    private HashMap<String, ArrayList<Node>> nodeList;
	// Rectangle for zoom box
    private Path2D rectangle;
    
    // Default constructor
    public Model()
    {
        regions = new HashMap<String, Region>();
        roads = new HashMap<String, Road>();
        scenarioFileName = "none";
        labelVisibility = true;
        xTranslate = 0.0;
        yTranslate = 0.0;
        xScale = 0.0;
        yScale = 0.0;
        width = 0;
        height = 0;
        maxE = 0.0;
        maxN = 0.0;
        minE = 0.0;
        minN = 0.0;
        fileIsLoaded = false;
        transparencyFactor = 0;
        filenames = new ArrayList<String>();
    }
    
    // Mutators
    public void setTranslation( Double xTranslate, Double yTranslate )
    {
        this.xTranslate += xTranslate;
        this.yTranslate += yTranslate;
    }
    
    public void setScale( Double zoomFactor )
    {
        xScale = xScale * zoomFactor;
        yScale = yScale * zoomFactor;
    }
    
    public void setHeight( Integer height )
    {
        this.height = height;
    }
    
    public void setWidth( Integer width )
    {
        this.width = width;
    }
    
    public void resetScale()
    {
        xTranslate = Math.abs(minE);
        yTranslate = -(Math.abs(maxN));
        Double oldHeight = (maxN - minN);
        Double oldWidth = (maxE - minE);
        Double newHeight = (oldHeight / oldWidth) * width;
        xScale = width / oldWidth;
        yScale = newHeight / oldHeight;
    }
    
    // Accessors
    
    /**
     * Returns regions read from file.
     * @return HashMap of regions read from file.
     */
    public String getScenarioFileName()
    {
        return scenarioFileName;
    }
    
    /**
     * Returns regions read from file.
     * @return HashMap of regions read from file.
     */
    public HashMap<String, Region> getRegions()
    {
        return regions;
    }
    
    /**
     * Returns roads read from file.
     * @return HashMap of roads read from file.
     */
    public HashMap<String, Road> getRoads()
    {
        return roads;
    }    

    /**
     * Returns X scale factor.
     * @return X scale factor.
     */    
    public Double getXScale()
    {
        return xScale;
    }

    /**
     * Returns Y scale factor.
     * @return Y scale factor.
     */      
    public Double getYScale()
    {
        return yScale;
    }

    /**
     * Returns X translation value.
     * @return X translation value.
     */      
    public Double getXTranslate()
    {
        return xTranslate;
    }

    /**
     * Returns Y translation value.
     * @return Y translation value.
     */    
    public Double getYTranslate()
    {
        return yTranslate;
    }
    
    /**
     * Returns minimum easting value.
     * @return minimum easting value.
     */    
    public Double getMinE()
    {
        return minE;
    }
    
    /**
     * Returns maximum easting value.
     * @return maximum easting value.
     */     
    public Double getMaxE()
    {
        return maxE;
    }

    /**
     * Returns minimum northing value.
     * @return minimum northing value.
     */     
    public Double getMinN()
    {
        return minN;
    }

    /**
     * Returns maximum northing value.
     * @return maximum northing value.
     */     
    public Double getMaxN()
    {
        return maxN;
    }

    /**
     * Returns height value.
     * @return height value.
     */     
    public Integer getHeight()
    {
        return height;
    }

    /**
     * Returns width value.
     * @return width value.
     */     
    public Integer getWidth()
    {
        return width;
    }

    /**
     * Returns whether to show labels.
     * @return Boolean flag.
     */  
    public boolean getLabelVisibility()
    {
        return labelVisibility;
    }

    /**
     * Set whether to show labels.
     * @return Boolean flag.
     */  
    public void setLabelVisibility( boolean labelVisibility )
    {
        this.labelVisibility = labelVisibility;
    }    
    
    /**
     * Checks whether a region has been selected.
     * @param x - x value to check.
     * @param y - y value to check.
     * Could add the functionality from the select road
     */     
    public Region selectRegion( Integer x, Integer y, AffineTransform tr )
    {
        try
        {
            Point2D clickPoint = new Point2D.Double( x, y );
            Point2D temp = tr.inverseTransform( clickPoint, null );
            for( Region r : getRegions().values() )
            {
                if( r.getPath().contains(temp) )
                {
                	if( r.getName().equals("Rail") )
                		return r;
                }
            }
        }
        catch ( Exception e )
        {
            System.out.println( "ERR: Error selectRegion - " + e.getMessage() );
        }   
        return null;
    }
    
    
    /**
     * TODO: Needs to fixed OR JUST REMOVE
     */
    private double distFrom( double e1, double n1, double e2, double n2 )
    {
	    double earthRadius = 3958.75;
	    double dLat = Math.toRadians( e2-e1 );
	    double dLng = Math.toRadians( n2-n1 );
	    double a =
            Math.sin( dLat/2 ) * Math.sin( dLat/2 ) +
	        Math.cos( Math.toRadians(e1)) * Math.cos( Math.toRadians(e2) ) *
	        Math.sin( dLng/2 ) * Math.sin( dLng/2 );
            
	    double c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a) );
	    double dist = earthRadius * c;

	    int meterConversion = 1609;

	    return ( new Double( dist * meterConversion ).floatValue() );
	}
    
    
    /**
     * Checks whether a road has been selected.
     * @param x - x value to check.
     * @param y - y value to check.
     */     
    public Road selectRoad( Integer x, Integer y, AffineTransform tr )
    {
        try
        {
        	Road tempSelectedRoad = null;
            Point2D clickPoint = new Point2D.Double( x, y );
            Point2D temp = tr.inverseTransform( clickPoint, null );
            double distanceMin = 1000.0;//Float.POSITIVE_INFINITY; // A massive number to start with
            String str = "";
            
            for( Road r : getRoads().values() )
            {
                for ( Path2D p : r.getRoad() )
                {
                	PathIterator pi = p.getPathIterator(null);
                	float[] values = new float[6];
                	float x1 = 0;
                	float y1 = 0;
                	float x2 = (float) temp.getX();
                	float y2 = (float) temp.getY();

                	while ( !pi.isDone() ) 
                	{
                	    int type = pi.currentSegment( values );
                        
                	    if ( type == PathIterator.SEG_LINETO ) 
                	    {
                	        x1 = values[0];
                	        y1 = values[1];
               	        
                	        //Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1))
                	        if( Math.min( Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)), distanceMin) < distanceMin )
                	        {
                                distanceMin = Math.min( Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)), distanceMin );
                	       
                	        	str =
                                    "Distance from " + r.getName() +
                                    " point (" + x1 + " " + y1 +
                                    ") to ( " + x2 + " " + y2 +
                                    ") is " + Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
                                    
                	        	tempSelectedRoad = r;
                	        }
                	    }
                	    
                	    pi.next();
                	}
                }  
               
            }
            tempSelectedRoad.setIsSelected(true);
            System.out.println(str);
            return tempSelectedRoad;
        }
        catch ( Exception e )
        {
            System.out.println( "ERR: Error no road near by the selection - " + e.getMessage() );
        }   
        return null;
    }    

    /**
     * Read a file containing scenario.
     * @param filename - Path and filename of file to read.
     * @throws IOException - If file cannot be read correctly.
     */      
    public void readScenarioFile( String filename ) throws IOException
    {
        // Clear any existing data
        regions.clear();
        roads.clear();
        
        String path = "";
        String[] tokens = filename.split( "\\\\{1}" ); // Back slashes for Windows
        //String[] tokens = filename.split( "\\/{1}" );  // Forward slashes for Mac
        
        
        for ( int i = 0; i < tokens.length - 1; i++ )
        {
        	path += tokens[i] + "\\";   // For Windows
        	//path += tokens[i] + "//";   // For Mac
        }
        // Set the scenario file loaded 
        this.scenarioFileName = filename; //tokens[tokens.length - 1];

        // Read the scenario file
        LineNumberReader input =
            new LineNumberReader(
                new FileReader( filename ) );

        // Read the files defined in scenario file
        String line = input.readLine();
        while( !line.equals( "finalend" ) )
        {
        	filenames.add( line );  // Add a filename to the list when read from scenario file
            readFile( path + line );
            line = input.readLine();
        }

        fileIsLoaded = true;
    }
    
    /**
     * Read a file containing northing and easting values in Australia Map Grid format.
     * @param filename - Path and filename of file to read.
     * @throws IOException - If file cannot be read correctly.
     */     
    public void readFile( String filename ) throws IOException
    {
    	//Setting flags to false
    	roadDataChanged = false;
    	regionDataChanged = false;
    	railDataChanged = false;

		// Create list of nodes
        ArrayList<Point2D> points = new ArrayList<Point2D>();
        ArrayList<Node> adjacencyList = new ArrayList<Node>();
        nodeList = new HashMap<String, ArrayList<Node>>();
        String nodeKey = "";
        
        LineNumberReader input =
            new LineNumberReader(
                new FileReader( filename ) );
        
        String line = input.readLine();
        String region = "";
        int count = 1;
        
        while( !line.equals( "finalend" ) )
        {
            maxE = 0.0;
            maxN = 0.0;
            minE = 0.0;
            minN = 0.0;
            
            Road road = null;
            String[] tokens = null;
            
            while( !line.equals( "end" ) )
            {
                tokens = line.split( "\\|{1}" );
                region = tokens[0];
                easting = Double.parseDouble( tokens[1] );
                northing = Double.parseDouble( tokens[2] );
            
                if( tokens.length == 7 && road == null )
                    road = new Road( region );
            
                maxE = Math.max( easting, maxE );
                maxN = Math.max( northing, maxN );
                minE = Math.min( easting, maxE );
                minN = Math.min( northing, maxN );
                
                Point2D p1 = new Point2D.Double( easting, northing );
                points.add( p1 );
                
                // Add second easting and northing if Line or Road
                if( tokens.length == 7)
                {
                    easting = Double.parseDouble( tokens[3] );
                    northing = Double.parseDouble( tokens[4] );
                    
                    maxE = Math.max( easting, maxE );
                    maxN = Math.max( northing, maxN );
                    minE = Math.min( easting, maxE );
                    minN = Math.min( northing, maxN );
                    
                    Point2D p2 = new Point2D.Double( easting, northing );
                    
                    // Creates a directed graph
                    nodeKey = p1.getX() + "," + p1.getY();
                    if ( nodeList.containsKey( nodeKey ) )
                    {
                        adjacencyList = nodeList.get( nodeKey );
                        Node node = new Node( region, p2.getX(), p2.getY(), 0.0 );  // TODO: Fix distance
                        adjacencyList.add( node );
                    }
                    else
                    {
                        adjacencyList = new ArrayList<Node>();
                        Node node = new Node( region, p2.getX(), p2.getY(), 0.0 );  // TODO: Fix distance
                        adjacencyList.add( node );
                        nodeList.put( nodeKey, adjacencyList );
                    }
                    
                    road.read ( p1, p2 );
                }
                
                line = input.readLine();
            }
            
            // Create new region for point data
            if ( tokens.length < 7 )
            {
            
            	Region r = new Region( region );
            
                // Set minimum and maximums for region
                r.setMaxE( maxE );
                r.setMaxN( maxN );
                r.setMinE( minE );
                r.setMinN( minN );
                
                // Give region unique name if rail or line
                if ( region.equals("Rail") || region.equals("Line") )
                {
                    region += count;
                    count++;
                }
                
                // Read latitude longitude data and create ArrayList of points
                r.read( points );
                
                // Add to regions HashMap
                regions.put( region, r );
                
                // Delete the ArrayList
                points.clear();
            }
            else if ( tokens.length == 7 ) // creating a road made of many paths
            {
                // Set minimum and maximums for region
                road.setMaxE( maxE );
                road.setMaxN( maxN );
                road.setMinE( minE );
                road.setMinN( minN );  

                roads.put( region, road );
            }
            
            // Read next line
            line = input.readLine();
        }
        
        // Reset minimum and maximums across all regions
        for ( Region r : regions.values() )
        {
            maxE = Math.max( r.getMaxE(), maxE );
            maxN = Math.max( r.getMaxN(), maxN );
            minE = Math.min( r.getMinE(), minE );
            minN = Math.min( r.getMinN(), minN );        
        }
    /*    
        // DEBUG: Print the connected nodes
        for ( Map.Entry<String, ArrayList<Node>> e : nodeList.entrySet() )
        {
            String key = e.getKey(); 
            ArrayList<Node> nodes = e.getValue();
            
            System.out.print(key + ": ");
            
            for ( Node n : nodes )
                System.out.print( "[" + n.getEasting() + "," + n.getNorthing() + "] -> " );
                
            System.out.println();
        }        
        */
        // Reset scale
        resetScale();
    }
    
    public void writeFile( String filename )
        throws IOException
    {
    	
    	ArrayList<Point2D> points = new ArrayList<Point2D>();
        
    	BufferedWriter output = new BufferedWriter(new FileWriter(filename));
        
    	if(roadDataChanged)
    	{
    		for(Road r :roads.values())
    		{
    			for(Path2D p : r.getRoad())
    			{
    				PathIterator pi = p.getPathIterator(null);
    				
    				float[] values = new float[6]; //TODO: Maybe set to 2 ?????????
                	float x1 = 0;
                	float y1 = 0;
                	float x2 = 0;
                	float y2 = 0;
    				
                	while ( !pi.isDone() )
                	{
	    				int type = pi.currentSegment( values );
                        
	             	    if(type == PathIterator.SEG_MOVETO)
	             	    {
	             	    	x1 = values[0];
	             	        y1 = values[1];
	             	    }
	    				if ( type == PathIterator.SEG_LINETO ) 
	             	    {
	             	        x2 = values[0];
	            	        y2 = values[1];
	             	    }
	    				
	    				pi.next();
                	}
                	output.write(
                        r.getName()+"|"+
    					(int)x1+"|"+(int)y1+"|"+
    					(int)x2+"|"+(int)y2+"|"+
    					"2|2\r"); //TODO: Get rid of the hardcoded values
                	
    			}
    			output.write("end\r");
    		}
    		output.write("finalend\r");
    			
    	}
        else if ( regionDataChanged )
    	{
            // TODO: Implement ...
    	}
        else if( railDataChanged )
    	{
    		// TODO: Implement ...
    	}
    	else
    	{
    		System.out.println("No changes to be saved"); //TODO: Change to a dialog box
    	}
    }
    
    public void setTransparency( int t )
    {
    	if( t <= 50 && t >= 0 )
    		this.transparencyFactor = t; 
    }
    
    public int getTransparencyFactor()
    {
    	return transparencyFactor;
    }
    
    public void setRectangle( Path2D rectangle )
    {
		this.rectangle = rectangle;
	}

	public Path2D getRectangle()
    {
		return rectangle;
	}

	public void setRoadDataChanged( boolean roadDataChanged )
    {
		this.roadDataChanged = roadDataChanged;
	}

	public boolean isRoadDataChanged()
    {
		return roadDataChanged;
	}

	public void setRegionDataChanged( boolean regionDataChanged )
    {
		this.regionDataChanged = regionDataChanged;
	}

	public boolean isRegionDataChanged()
    {
		return regionDataChanged;
	}

	public void setRailDataChanged( boolean railDataChanged )
    {
		this.railDataChanged = railDataChanged;
	}

	public boolean isRailDataChanged()
    {
		return railDataChanged;
	}
}
